package tetris;

import io.Recorder;

import java.io.IOException;
import java.util.Random;

import ui.FramePane;

public class Tetris implements Runnable {

	private int rows, cols;
	private int top;
	private boolean[][] piled;
	private FramePane pane;
	private int[][] current = new int[4][2], tmp = new int[4][2];
	private Random rnd = new Random();
	private int blockID, blockIDPreview = -1, type, rotation;
	double score, elim;
	final double initSpeed = 500;
	double speed = initSpeed;
	private int offX, offY;
	private volatile boolean stop, paused, started;

	private long time;
	private int minutes = 1;
	private Recorder recorder;
	private double highestScore, highestElim;

	private int[] eliminated = {-1, -1, -1, -1};

	public Tetris (FramePane pane) {
		this.pane = pane;
		try {
			recorder = new Recorder();
			highestScore = recorder.readHighestScore();
			highestElim = recorder.readHighestElim();
		} catch (IOException e) {
			e.printStackTrace();
		}
	}

	public void newGame () {
		minutes = 1;
		score = 0;
		elim = 0;
		if (recorder != null) {
			try {
				highestScore = recorder.readHighestScore();
				highestElim = recorder.readHighestElim();
			} catch (IOException e) {
				e.printStackTrace();
			}
		}
		updateScoreBoard();
		updateElimBoard();
		speed = initSpeed;
		stop = false;
		paused = false;
		rows = pane.getRows() + 2;
		cols = pane.getCols() + 2;
		top = rows - 1;
		piled = new boolean[rows][cols];
		for (int i = 0; i < rows; i++) {
			piled[i][0] = true;
			piled[i][cols - 1] = true;
		}
		for (int i = 1; i < cols - 1; i++) {
			piled[0][i] = true;
			piled[rows - 1][i] = true;
		}
	}

	public void closeRecorder () {
		try {
			recorder.close();
		} catch (IOException e) {
			e.printStackTrace();
		}
	}

	public void stop () {
		stop = true;
		paused = false;
		blockIDPreview = -1;
	}

	public synchronized void reset () {
		for (int i = 1; i < rows - 1; i++) {
			for (int j = 1; j < cols - 1; j++) {
				piled[i][j] = false;
			}
		}
		pane.setPreview(getBlockPreview());
	}

	private synchronized void newBlockPreview () {
		blockIDPreview = rnd.nextInt(TetrisBlock.B.length);
		pane.setPreview(getBlockPreview());
	}

	private synchronized void newBlock () {
		blockID = blockIDPreview;
		type = blockID / 4 * 4;
		rotation = blockID - type;
		offX = cols / 2 - 1;
		offY = 1;
		int[][] block = TetrisBlock.B[blockID];
		for (int i = 0; i < 4; i++) {
			current[i][0] = block[i][0] + offY;
			current[i][1] = block[i][1] + offX;
			tmp[i][0] = current[i][0];
			tmp[i][1] = current[i][1];
		}
	}

	private void shiftLeft2 (int n) {
		for (int i = 0; i < tmp.length; i++) {
			tmp[i][1] -= n;
		}
	}

	public boolean shiftLeft (int n) {
		if (!started) {
			return true;
		}
		if (paused) {
			return true;
		}
		shiftLeft2(n);
		if (!hit()) {
			confirm();
			offX -= n;
			return true;
		} else {
			rollBack();
			return false;
		}
	}

	private void shiftRight2 (int n) {
		for (int i = 0; i < tmp.length; i++) {
			tmp[i][1] += n;
		}
	}

	public boolean shiftRight (int n) {
		if (!started) {
			return true;
		}
		if (paused) {
			return true;
		}
		shiftRight2(n);
		if (!hit()) {
			confirm();
			offX += n;
			return true;
		} else {
			rollBack();
			return false;
		}
	}

	private synchronized void shiftDown2 () {
		for (int i = 0; i < tmp.length; i++) {
			tmp[i][0]++;
		}
	}

	private void shiftUp (int n) {
		for (int i = 0; i < tmp.length; i++) {
			tmp[i][0] -= n;
		}
	}

	public synchronized boolean shiftDown () {
		if (!started) {
			return true;
		}
		if (paused) {
			return true;
		}
		shiftDown2();
		if (!hit()) {
			confirm();
			offY++;
			return true;
		} else {
			rollBack();
			return false;
		}
	}

	private synchronized void confirm () {
		for (int i = 0; i < current.length; i++) {
			piled[current[i][0]][current[i][1]] = false;
		}
		for (int i = 0; i < current.length; i++) {
			current[i][0] = tmp[i][0];
			current[i][1] = tmp[i][1];
		}
		for (int i = 0; i < current.length; i++) {
			piled[current[i][0]][current[i][1]] = true;
		}
	}

	private void rollBack () {
		for (int i = 0; i < current.length; i++) {
			tmp[i][0] = current[i][0];
			tmp[i][1] = current[i][1];
		}
	}

	public synchronized void rotateClockwise () {
		if (!started) {
			return;
		}
		if (paused) {
			return;
		}
		int tmpRotation = rotation;
		rotation = (rotation + 1) % 4;
		int[][] next = TetrisBlock.B[type + rotation];
		for (int i = 0; i < tmp.length; i++) {
			tmp[i][0] = offY + next[i][0];
			tmp[i][1] = offX + next[i][1];
		}
		if (!hit()) {
			confirm();
		} else {
			shiftLeft2(1);
			if (!hit()) {
				confirm();
				offX--;
				return;
			} else {
				if (type == 0) {
					shiftRight2(2);
					if (!hit()) {
						confirm();
						offX++;
						return;
					} else {
						shiftRight2(1);
						if (!hit()) {
							confirm();
							offX += 2;
							return;
						} else {
							shiftLeft2(3);
						}
					}
				}
				if (rotation % 2 == 1) {
					shiftRight2(1);
					shiftUp(1);
					if (!hit()) {
						confirm();
						offY--;
						return;
					} else {
						if (type == 0) {
							shiftUp(1);
							if (!hit()) {
								confirm();
								offY--;
								return;
							} else {
								rollBack();
								rotation = tmpRotation;
							}
						} else {
							rollBack();
							rotation = tmpRotation;
						}
					}
				} else {
					rollBack();
					rotation = tmpRotation;
				}
			}
		}
	}

	public void rotateAClock () {
		// to complete
	}

	public synchronized void drop () {
		if (!started) {
			return;
		}
		if (paused) {
			return;
		}
		if (hit()) {
			return;
		}
		while (!hit()) {
			shiftDown2();
			offY++;
		}
		shiftUp(1);
		offY--;
		confirm();
	}

	@Override
	public void run () {
		started = true;
		time = System.currentTimeMillis();
		newBlockPreview();
		while (true) {
			if (stop) {
				started = false;
				return;
			}
			newBlock();
			newBlockPreview();
			if (stop) {
				started = false;
				return;
			}
			if (hit2()) {
				confirm();
				break;
			} else {
				confirm();
			}
			do {
				if (stop) {
					started = false;
					return;
				}
				while (paused) {
					if (stop) {
						started = false;
						return;
					}
					try {
						Thread.sleep(1);
					} catch (InterruptedException e) {
						e.printStackTrace();
					}
				}
				try {
					Thread.sleep((long) speed);
				} catch (InterruptedException e) {
					e.printStackTrace();
				}
			} while (shiftDown());
			// update the top -- the highest row which has over 50% occupations.
			int blockHeight = offY;
			for (int[] element : current) {
				if (element[0] > blockHeight) {
					blockHeight = element[0];
				}
			}
			for (int highest = offY; highest < top && highest <= blockHeight; highest++) {
				int count = 0;
				for (int i = 1; i < cols - 1; i++) {
					if (piled[highest][i]) {
						count++;
					}
				}
				if (count >= (cols - 2) / 2) {
					top = highest;
					break;
				}
			}

			int e = eliminate();
			updateScore(e);
			updateElim(e);
			updateSpeed(e);
		}
		int e = eliminate();
		updateScore(e);
		updateElim(e);
		updateSpeed(e);
		stop = true;
		pane.tetrisNormallyGameOver();
		started = false;
	}

	public boolean isStarted () {
		return started;
	}

	public long getTime () {
		if (!started) {
			return 0;
		}
		return System.currentTimeMillis() - time;
	}

	private void updateScore (int eliminated) {
		if (eliminated > 0) {
			score += (5.0 - 4.0 * top / rows) * (5.0 - 4.0 * speed / initSpeed) * eliminated * (eliminated + 1.0) * (2.0 * eliminated + 1.0) / 6.0;
			if (score > highestScore) {
				if (recorder != null) {
					try {
						recorder.updateHighestScore(score);
					} catch (IOException e) {
						e.printStackTrace();
					}
				}
			}
			updateScoreBoard();
		}
	}

	private void updateScoreBoard () {
		pane.setScore((int) score, (int) highestScore);
	}

	private void updateElim (int eliminated) {
		if (eliminated > 0) {
			pane.initEliminationMovie();
			elim += eliminated;
			if (elim > highestElim) {
				if (recorder != null) {
					try {
						recorder.updateHighestElim(elim);
					} catch (IOException e) {
						e.printStackTrace();
					}
				}
			}
			updateElimBoard();
		}
	}

	private void updateElimBoard () {
		pane.setElim((int) elim, (int) highestElim);
	}

	private void updateSpeed (int eliminated) {
		speed *= 1.0 - eliminated / 200.0;
		if ((System.currentTimeMillis() - time) / 60000 > minutes) {
			speed *= 0.999;
			minutes = (int) ((System.currentTimeMillis() - time) / 60000);
		}
	}

	public void pause () {
		if (started) {
			paused = true;
		}
	}

	public void resume () {
		if (started) {
			paused = false;
		}
	}

	public void setPaused (boolean paused) {
		if (started) {
			this.paused = paused;
		}
	}

	public boolean isPaused () {
		return paused;
	}

	public void changePausedEasy () {
		if (started) {
			paused = !paused;
		}
	}

	public double getSpeed () {
		return speed;
	}

	public double getScore () {
		return score;
	}

	public void setScore (double score) {
		this.score = score;
		updateScoreBoard();
	}

	public void setElim (double elim) {
		this.elim = elim;
		updateElimBoard();
	}

	public double getInitSpeed () {
		return initSpeed;
	}

	private int eliminate () {
		int c = 0;
		out: for (int i = offY; i < offY + 4 && i < rows - 1; i++) {
			for (int j = 1; j < cols - 1; j++) {
				if (!piled[i][j]) {
					continue out;
				}
			}
			eliminated[c] = i - 1;
			c++;

			for (int k = 1; k < cols - 1; k++) {
				for (int j = i; j > 2; j--) {
					piled[j][k] = piled[j - 1][k];
				}
				piled[1][k] = false;
			}
		}
		if (c > 0) {
			for (int i = c; i < eliminated.length; i++) {
				eliminated[i] = -1;
			}
			top += c;
		}
		return c;
	}

	public boolean[][] getStatus () {
		return piled;
	}

	private int[][] getBlockPreview () {
		if (blockIDPreview == -1) {
			return null;
		} else if (blockIDPreview != 1 && blockIDPreview != 3) {
			return TetrisBlock.B[blockIDPreview];
		} else {
			int[][] b = new int[4][2];
			for (int i = 0; i < 4; i++) {
				b[i][0] = TetrisBlock.B[blockIDPreview][i][0];
				b[i][1] = TetrisBlock.B[blockIDPreview][i][1] - 2;
			}
			return b;
		}
	}

	private synchronized boolean hit2 () {
		for (int[] element : tmp) {
			if (piled[element[0]][element[1]]) {
				return true;
			}
		}
		return false;
	}

	private synchronized boolean hit () {
		out: for (int[] element : tmp) {
			if (element[0] > rows - 1 || element[1] < 0) {
				return true;
			}
			if (piled[element[0]][element[1]]) {
				for (int i = 0; i < current.length; i++) {
					if (element[0] == current[i][0] && element[1] == current[i][1]) {
						continue out;
					}
				}
				return true;
			}
		}
		return false;
	}

	public int[] getEliminatedRows () {
		return eliminated;
	}

	public double getElims () {
		return elim;
	}
}
